/* The EtermView implementation.

   For legal stuff see the file COPYRIGHT.  */

#import <dpsclient/dpsclient.h>
#import <libc.h>
#import <mach/mach.h>
#import <stdio.h>
#import <stdlib.h>
#import <sys/ioctl.h>
#import <sys/param.h>

#import "EtermView.h"
#import "EmacsApp.h"
#import "etermSupport.h"
#import "defaults.h"
#import "display.h"

extern char **environ;

/* These are the window size limits imposed by GNUEmacs.  */
#define MINWIDTH 10
#define MINHEIGHT 5
#define MAXWIDTH 300
#define MAXHEIGHT 300

/* Function prototypes for display.pswm */
void *new_display_rock (EtermView *theView);
void input_from_emacs (int fd, void *theDisplay);

/* Function prototypes for keycode.c */
void kc_init (void);
int kc_keyforcode (int code, int flags);

/* write(2), except it will keep retrying upon partial success.  */
static int
write_safely (int fd, char *buf, int n)
{
  int i, start = 0;

  while (start < n)
    {
      i = write (fd, buf + start, n - start);
      if (i == -1)
	return i; /* Some error */
      start += i;
    }
  return n;
}

/* Handle a connection to the event server socket */
static void
connection_from_emacs (int fd, void *rock)
{
  EtermView *view = (EtermView *) rock;
  int newfd = accept_server_connection (fd);

  if (newfd == -1)
    return;

  if ([view eventChannel])
    close (newfd);
  else
    [view setEventChannel: fdopen (newfd, "w")];
}

@implementation EtermView

+ initialize
{
  static NXDefaultsVector etermDefaults =
    {
      {"NXAutoLaunch", "NO" },
      {"NXFixedPitchFontSpacing", "0.95" },
      {"HideOnAutoLaunch", "NO" },
      {"EmacsPath", DEFAULT_EMACS_PATH},
      {"LispPath", DEFAULT_LISP_PATH},
      {NULL},
    };
  const char *sTypes[2], *rTypes[2];

  sTypes[0] = NXAsciiPboardType;
  sTypes[1] = NULL;
  rTypes[0] = NXAsciiPboardType;
  rTypes[1] = NULL;

  NXRegisterDefaults ([NXApp appName], etermDefaults);
  [NXApp registerServicesMenuSendTypes: sTypes andReturnTypes: rTypes];
  kc_init ();

  return self;
} /* +initialize */

-initFrame: (const NXRect *) newFrame
{
  self = [super initFrame: newFrame];
  [self setFlipped: YES];
#if 0
  [self notifyAncestorWhenFrameChanged: YES];
#endif

  spacing = atof (NXGetDefaultValue ([NXApp appName],
				     "NXFixedPitchFontSpacing"));
  /* Check for sane values of spacing.  */
  if (spacing < 0.9)
    spacing = 0.9;
  else if (spacing > 2.0)
    spacing = 2.0;
  [self setFont: [self font]];
  [[NXApp fontManager] setSelFont: [self font] isMultiple: NO];
  return self;
} /* -initFrame: */

-(float) spacing
{
  return spacing;
}

-setSpacing: (float) newSpacing
{
  char val[20];

  spacing = newSpacing;
  [self setFont: [self font]];
  sprintf (val, "%f", newSpacing);
  NXWriteDefault ([NXApp appName], "NXFixedPitchFontSpacing", val);
  return self;
}

-font
{
  return [Font userFixedPitchFontOfSize: (float) 0 matrix: NX_FLIPPEDMATRIX];
} /* -font */

-setFont: newFont
{
  NXCoord ascender, descender, lineHeight;
  NXRect frameRect, contentRect;
  Font *screenfont;
  int leading;

  /* NXTextFontInfo is broken for screen fonts, so call it before converting
     the font to a screenfont.  */
  NXTextFontInfo (newFont , &ascender, &descender, &lineHeight);
  screenfont = [newFont screenFont];
  if (screenfont)
    newFont = screenfont;

  leading = (spacing - 1.0) * lineHeight;
  fontHeight = (int) (lineHeight * spacing + leading);
  fontWidth = (float) ([newFont getWidthOf : "  "]
		       - [newFont getWidthOf : " "] + 0.5);
  fontDescender = (int) (descender + leading);

  [newFont set];
  if (screenfont)
    set_font ();
  else
    fix_font (fontWidth);
  displayFont = newFont;

  /* Snap the window size to a 1 x 1 character grid.  */
  [window getFrame: &frameRect];
  [self windowWillResize: self toSize: &frameRect.size];
  [Window getContentRect: &contentRect forFrameRect: &frameRect
          style: [window style]];
  [window sizeWindow: contentRect.size.width : contentRect.size.height];
  [self windowDidResize: self];
  [self display];

  return self;
} /* -setFont: */

/* Sent by fontManager.  */
-changeFont: sender
{
  id selectedFont;

  /* Get the font selected in the FontPanel.  */
  selectedFont = [[NXApp fontManager]
		  convertFont: [[NXApp fontManager] selFont]];
  [self setFont: selectedFont];
  [Font setUserFixedPitchFont: selectedFont];
  return self;
} /* -changeFont: */

-(FILE *) eventChannel;
{
  return eventChannel;
} /* -eventChannel */

-setEventChannel: (FILE *) fp;
{
  eventChannel = fp;
  return self;
} /* -setEventChannel: */

/* Constrain window so that the content view has an integral width in terms
   of characters.  XXX this method needs some cleaning up to stop the window
   from flashing while resizing.  */
-windowWillResize: sender toSize: (NXSize *) frameSize;
{
  static int oldw = 0, oldh = 0;
  int neww, newh;
  NXRect frameRect, contentRect;
  int newwidth, newheight;
  int style = [window style];

  NXSetRect (&frameRect, 0.0, 0.0, frameSize->width, frameSize->height);
  [Window getContentRect: &contentRect forFrameRect: &frameRect style: style];

  newwidth = (int) (contentRect.size.width + fontWidth / 2 - BORDER_WIDTH * 2);
  newheight = (int) (contentRect.size.height
		     + fontHeight / 2 - BORDER_WIDTH * 2);

  newwidth -= newwidth % fontWidth;
  newheight -= newheight % fontHeight;

  if (newwidth < MINWIDTH * fontWidth)
    newwidth = MINWIDTH * fontWidth;
  if (newwidth > MAXWIDTH * fontWidth)
    newwidth = MAXWIDTH * fontWidth;

  if (newheight < MINHEIGHT * fontHeight)
    newheight = MINHEIGHT * fontHeight;
  if (newheight > MAXHEIGHT * fontHeight)
    newheight = MAXHEIGHT * fontHeight;

  contentRect.size.width = (NXCoord) newwidth + BORDER_WIDTH * 2;
  contentRect.size.height = (NXCoord) newheight + BORDER_WIDTH * 2;
  [Window getFrameRect:&frameRect forContentRect:&contentRect style:style];

  frameSize->width = frameRect.size.width;
  frameSize->height = frameRect.size.height;

  newh = newheight / fontHeight;
  neww = newwidth / fontWidth;
  if (newh != oldh || neww != oldw)
    {
      [self showTitle: newh : neww];
      oldh = newh;
      oldw = neww;
    }
  return self;
} /* -windowWillResize:toSize: */

/* Convert size to number of characters in content view.  Inform child emacs
   process of our new size.  */
-windowDidResize: sender
{
  NXRect frameRect, contentRect;
  int style = [window style];
  struct winsize winsize;

  [window getFrame: &frameRect];
  [Window getContentRect: &contentRect forFrameRect: &frameRect style: style];

  lines = (int) ((contentRect.size.height - BORDER_WIDTH * 2) / fontHeight);
  cols =  (int) ((contentRect.size.width - BORDER_WIDTH * 2) / fontWidth);
  [self showTitle: lines : cols];

  /* emacs doesn't do anything with a TIOCCSWINSZ if LINES nor COLS change,
     so send a redraw screen in that case.  */
  winsize.ws_row = lines;
  winsize.ws_col = cols;
  winsize.ws_xpixel = (int) contentRect.size.width;
  winsize.ws_ypixel = (int) contentRect.size.height;
  ioctl (masterChannel, TIOCSWINSZ, &winsize);
  if (eventChannel)
    fprintf (eventChannel, "(redraw-display)\n");
  return self;
} /* -windowDidResize: */

/* Start up the child emacs process, perhaps on a file.  */
-startEmacs: (char **) files : (int) nfiles
{
  int master, slave, ptynumber, portno, i, n;
  char **args, **env;
  char *lisp, *path;

  /* Grab a pty/tty pair */
  create_channel (&master, &slave, &ptynumber);
  masterChannel = master;

  /* Create the server socket */
  eventServerSocket = create_server_socket (&portno);
  DPSAddFD (eventServerSocket, connection_from_emacs,
	    (void *) self, NX_RUNMODALTHRESHOLD);

  /* Frob the environment */
  env = patch_env (environ, portno);

  /* Make sure the size is set correctly */
  [self windowDidResize: self];

  args = calloc (nfiles + 6, sizeof (char *));
  args[0] = "emacs";

  path = malloc (MAXPATHLEN + 1);
  lisp = NXGetDefaultValue ([NXApp appName], "LispPath");
  args[1] = "-l";
  args[3] = "-f";
  args[4] = "start-event-server";
  n = 5;
  if (strrchr (lisp, '/'))
    args[2] = lisp;
  else
    {
      if ([[NXBundle mainBundle] getPath: path forResource: lisp ofType: NULL])
	args[2] = path;
      else
	n = 1;
    }

  for (i = 0; i < nfiles; i++, n++)
    args[n] = files[i];

  /* Fork off the Emacs */
  fork_shell (NXGetDefaultValue ([NXApp appName], "EmacsPath"),
	      args, env, slave);
  free (path);
  free (args);
  close (slave);

  DPSAddFD (master, input_from_emacs, new_display_rock (self),
	    NX_RUNMODALTHRESHOLD);
  [window display];
  return self;
} /* -startEmacs: */

-(BOOL) acceptsFirstResponder
{
  return YES;
} /* -acceptsFirstResponder */

/* Keyboard input is given to the child emacs process.  XXX this method needs some cleeaning up.  */
-keyDown: (NXEvent *) theEvent;
{
  NXEvent eventbuf;
  char buf[1024];
  int i = 0;
  int code;
  int start = 0;

  if (theEvent->flags & NX_NUMERICPADMASK
      && theEvent->data.key.charCode >= 0xac
      && theEvent->data.key.charCode <= 0xaf)
    {
      /* Arrow keys (left, up, right, down) */
      if (theEvent->flags & NX_SHIFTMASK)
	buf[i++] = "\302\326\306\026"[theEvent->data.key.charCode - 0xac];
      else if (theEvent->flags & NX_CONTROLMASK)
	buf[i++] = "\001\274\005\276"[theEvent->data.key.charCode - 0xac];
      else if (theEvent->flags & NX_ALTERNATEMASK)
	buf[i++] = "\202\220\206\216"[theEvent->data.key.charCode - 0xac];
      else
	buf[i++] = "\002\020\006\016"[theEvent->data.key.charCode - 0xac];
    }
  else if (theEvent->flags & NX_NUMERICPADMASK
	   && theEvent->data.key.charCode == 0x03)
    {
      /* Enter key */
      buf[i++] = '\r';
    }
  else if (theEvent->flags & NX_ALTERNATEMASK)
    {
      /* Handle ALT key as a META key */
      code = kc_keyforcode(theEvent->data.key.keyCode, theEvent->flags);
      if (code != -1) {
	buf[i++] = code | 0x80;
      }
    }
  else buf[i++] = theEvent->data.key.charCode;

  /* Grab as many keypressed events as we can from a modal event loop */
  while (i < sizeof (buf)
	 && (theEvent = [NXApp peekNextEvent: NX_ALLEVENTS into: &eventbuf])
	 && theEvent->type == NX_KEYDOWN
	 && !(theEvent->flags & NX_COMMANDMASK))
    {
      theEvent = [NXApp getNextEvent: NX_ALLEVENTS];

      if (theEvent->flags & NX_NUMERICPADMASK
	  && theEvent->data.key.charCode >= 0xac
	  && theEvent->data.key.charCode <= 0xaf)
	{
	  /* Arrow keys (left, up, right, down) */
	  if (theEvent->flags & NX_SHIFTMASK)
	    buf[i++] = "\302\326\306\026"[theEvent->data.key.charCode - 0xac];
	  else if (theEvent->flags & NX_CONTROLMASK)
	    buf[i++] = "\001\274\005\276"[theEvent->data.key.charCode - 0xac];
	  else if (theEvent->flags & NX_ALTERNATEMASK)
	    buf[i++] = "\202\220\206\216"[theEvent->data.key.charCode - 0xac];
	  else
	    buf[i++] = "\002\020\006\016"[theEvent->data.key.charCode - 0xac];
	}
      else if (theEvent->flags & NX_NUMERICPADMASK
	       && theEvent->data.key.charCode == 0x03)
	{
	  /* Enter key */
	  buf[i++] = '\r';
	}
      else if (theEvent->flags & NX_ALTERNATEMASK)
	{
	  /* Handle ALT key as a META key */
	  code = kc_keyforcode (theEvent->data.key.keyCode, theEvent->flags);
	  if (code != -1)
	    buf[i++] = code | 0x80;
	}
      else buf[i++] = theEvent->data.key.charCode;
    }

  write_safely (masterChannel, buf, i);
  obscure_cursor ();

  return self;
} /* -keyDown: */

/* Mouse input is converted to a character position and given to the child
   emacs process, but only if the emacs has opened an event port.  */
-mouseEvent: (NXEvent *) theEvent : (int) button;
{
  int x, y;
  char buf[35];

  if (!eventChannel)
    return self;

  [self convertPoint: &theEvent->location fromView:nil];
  x = (int) ((theEvent->location.x - BORDER_WIDTH) / fontWidth);
  y = (int) ((theEvent->location.y - BORDER_WIDTH) / fontHeight);

  if (x < 0)
    x = 0;
  if (y < 0)
    y = 0;
  if (x >= cols)
    x = cols - 1;
  if (y >= lines)
    y = lines - 1;

  sprintf (buf, "\030%c(%d %d %d 1)\r", 0,
	   (button + ((theEvent->flags & NX_SHIFTMASK) ? 8 : 0)
	    + ((theEvent->flags & NX_CONTROLMASK) ? 16 : 0)
	    + ((theEvent->flags & NX_ALTERNATEMASK) ? 32 : 0)), x, y);
  write_safely (masterChannel, buf, 2+strlen (buf+2));

  return self;
} /* -mouseEvent:: */


-mouseDown: (NXEvent *) theEvent;
{
  [self mouseEvent: theEvent: 1];
  return self;
} /* -mouseDown: */

-mouseUp: (NXEvent *) theEvent;
{
  [self mouseEvent: theEvent: 1 + 128];
  return self;
} /* -mouseUp */

-rightMouseDown: (NXEvent *) theEvent;
{
  [self mouseEvent: theEvent: 4];
  return self;
} /* rightMouseDown: */

-rightMouseUp: (NXEvent *) theEvent;
{
  [self mouseEvent: theEvent: 4 + 128];
  return self;
} /* rightMouseUp: */

-(BOOL) emacsOn
{
  if (eventChannel)
    return YES;
  else
    return NO;
} /* -emacsOn */

-(BOOL) newFile: (const char *) path
{
  if (!eventChannel)
    return NO;
  fprintf (eventChannel, "(find-file \"%s\")\n", path);
  fflush (eventChannel);
  return YES;
} /* -newFile: */
    
/* Quit, cut, copy, paste, and undo messages are sent to the child's event
   port.  */
-quitEmacs
{
  if (!eventChannel)
    return nil;
  fprintf (eventChannel, "(event-quit)\n");
  fflush (eventChannel);
  return self;
} /* -quitEmacs */

-pasteboard
{
  return currentPasteboard;
} /* -pasteboard */

-appPasteboard
{
  if (!mainPasteboard)
    mainPasteboard = [Pasteboard new];
  return mainPasteboard;
} /* -appPasteboard */

-setPasteboard: pboard
{
  currentPasteboard = pboard;
  return self;
} /* -setPasteboard: */

-(BOOL) sendEmacsEvent: (char *) theEvent
{
  if (!eventChannel)
    return NO;
  fprintf (eventChannel, "%s\n", theEvent);
  fflush (eventChannel);
  return YES;
} /* -(BOOL) sendEmacsEvent: */

-undo: sender;
{
  [self sendEmacsEvent: "(undo)"];
  return self;
} /* -undo: */

-save: sender;
{
  [self sendEmacsEvent: "(save-buffer)"];
  return self;
} /* -save: */

-saveAll: sender;
{
  [self sendEmacsEvent: "(save-some-buffers t)"];
  return self;
} /* -saveAll: */

-cut: sender;
{
  [self cutTo: [self appPasteboard]];
  return self;
} /* -cut: */

-copy: sender;
{
  [self copyTo: [self appPasteboard]];
  return self;
} /* -copy: */

-paste: sender;
{
  [self pasteFrom: [self appPasteboard]];
  return self;
} /* -paste: */

-open: sender;
{
  int i;
  char *path;
  const char *directory;
  const char *const *fileNames;

  if (!eventChannel)
    return self;
  if (!openPanel)
    openPanel = [OpenPanel new];
  [openPanel allowMultipleFiles: YES];
  [openPanel chooseDirectories: NO];
  if ([openPanel runModal])
    {
      directory = [openPanel directory];
      fileNames = [openPanel filenames];
      if (fileNames)
	{
	  for (i = 0; fileNames[i]; i++)
	    {
	      path = malloc (2 + strlen (directory) + strlen (fileNames[i]));
	      strcat (strcat (strcpy (path, directory), "/"), fileNames[i]);
	      fprintf (eventChannel, "(find-file \"%s\")\n", path);
	      fflush (eventChannel);
	      free (path);
	    }
	}
    }
  return self;
} /* -open: */

-saveAs: sender;
{
  const char *path;

  if (!eventChannel)
    return self;
  if (!savePanel)
    savePanel = [SavePanel new];
  if ([savePanel runModal])
    {
      path = [savePanel filename];
      if (path)
	{
	  fprintf (eventChannel, "(write-file \"%s\")\n", path);
	  fflush (eventChannel);
	}
    }
  return self;
} /* -saveAs: */

-(BOOL) cutTo: pasteboard
{
  return [[self setPasteboard: pasteboard] sendEmacsEvent: "(event-cut)"];
} /* -(BOOL) cutTo: */

-(BOOL) copyTo: pasteboard
{
  return [[self setPasteboard: pasteboard] sendEmacsEvent: "(event-copy)"];
} /* -(BOOL) copyTo: */

-(BOOL) pasteFrom: pasteboard
{
  const NXAtom *types, *p;
  char *data;
  int len;

  if (!eventChannel || !pasteboard)
    return NO;
  types = [pasteboard types];
  for (p = types; *p && strcmp (*p, NXAsciiPboardType); p++);

  if (*p && [pasteboard readType: NXAsciiPboardType data: &data length: &len]
      && len)
    {
      int c;
      char *tosend = data;

      fputs("(event-paste \"", eventChannel);

      /* Write out string, quoting quote, backslash, and newline */
      for (tosend = data; len--; tosend++)
	{
	  switch (c = *tosend)
	    {
	    case '\n':
	      fputs ("\\n", eventChannel);
	      break;
	    case '\\':
	    case '\"':
	      putc('\\', eventChannel);
	      /* FALL THROUGH */
	    default:
	      putc (c, eventChannel);
	      break;
	    }
	}
      fputs ("\")\n", eventChannel);
      fflush (eventChannel);
      vm_deallocate (task_self (), (vm_address_t) data, len);
    }
  return YES;
} /* -(BOOL) pasteFrom: */

-getDimensions: (int *) linesPtr : (int *) colsPtr
{
  *linesPtr = lines;
  *colsPtr = cols;
  return self;
} /* -getDimensions:: */

-(Font *) getDisplayFont:
  (int *) heightPtr :
  (int *) widthPtr :
  (int *) descenderPtr
{
  *heightPtr = fontHeight;
  *widthPtr = fontWidth;
  *descenderPtr = fontDescender;
  return displayFont;
} /* -getDisplayFont::: */

-showTitle: (int) newLines : (int) newColumns
{
  char *newtitle;

  if (!titleprefix)
    return self;
  newtitle = malloc (22 + strlen (titleprefix));
  sprintf (newtitle, "%s (%dx%d)", titleprefix, newColumns, newLines);
  [(Window *) window setTitle: newtitle];
  free (newtitle);
  return self;
} /* -showTitle:: */

-setTitle: (char *) title
{
  titleprefix = title;
  [self showTitle: lines : cols];
  return self;
} /* -setTitle: */

-validRequestorForSendType: (NXAtom) typeSent
 andReturnType: (NXAtom) typeReturned
{
  /* Check to make sure that the types are ones that we can handle.  */
  if (eventChannel && (typeSent == NXAsciiPboardType || typeSent == NULL)
      && (typeReturned == NXAsciiPboardType || typeReturned == NULL))
    return self;
  /* Otherwise, return the default.  */
  return [super validRequestorForSendType: typeSent
                andReturnType: typeReturned];
} /* -validRequestorForSendType:andReturnType: */

-(BOOL) writeSelectionToPasteboard: pboard types: (NXAtom *) types
{
  const NXAtom *p;

  if (eventChannel && pboard)
    {
      for (p = types; *p && strcmp (*p, NXAsciiPboardType); p++);
      if (*p)
	{
/* We should have the pasteboard data ready before returning here, how?  */
#if 0
	  [pboard declareTypes: &NXAsciiPboardType
	          num: 1
	          owner: NULL];
#endif
	  return [self copyTo: pboard];
	}
    }
  return NO;
} /* -writeSelectionToPasteboard:types: */

-readSelectionFromPasteboard: pboard
{
  [self sendEmacsEvent: "(kill-region (mark) (point))"];
  [self pasteFrom: pboard];
  return self;
} /* -readSelectionFromPasteboard: */

@end
